/*
WEB服务的中间件
针对抵御抓包重放攻击
同时使用timestamp和nonce的方案
1、nonce有客户端随机生成的一次性使用的字符串，每次请求带有该参数（可HASH(token + 序号 + 时间戳 + deviceNo) 等）。首次使用允许，第二次出现则判别为非法请求。
2、服务器端存储nonce（），如果发现已有，则为重复接收，认为是无效的（也可以允许重复次数）。
3、服务器存储nonce，可以用redis 或 map（重启会丢数据），有效期到后清空
4、时间戳作为参数，（传送时间戳参数，由时间戳和token 进行加密，这样可以防止时间戳被篡改）， 比如60秒有效。但是难以保证服务器和客户端的时钟是一致的。
5、时间戳有效范围内，比如24小时，如果时间戳超过该范围，则认为无效请求。
6、参数放置于POST请求头部，检测主要针对post请求/put请求
7、所以https本身就是防止重放攻击的，除非能复制socket，或者进行中间人攻击。
8、redis存储：key = nonce + user_id  ，value = timestamp
*/
package middleware

import (
	"fmt"
	"gitee.com/baal1990/gitee.com/baal1990/pkg/cache"
	"gitee.com/baal1990/gitee.com/baal1990/pkg/model"
	"github.com/gin-gonic/gin"
	"net/http"
	"strings"
)

const (
	REDIS_KEY_TOURIST_HASH         = "redis_key_tourist_hash" //游客设备号和token的映射关系
	REDIS_FIELD_SAFETY             = "safety"                 //安全记录域
	REDIS_KEY_APIFINGERPRINT_TIMES = "api_fingerprint_times_" //API数据包指纹和访问次数的记录，用于防止抓包重放攻击	fingerPrint
)

var (
	ApiPackageMaxCnt   int  //重复接收API包次数上线
	ApiPackageKeepTime int  //API数据包指纹保存有效时间 单位秒
	SafetyCheckEnable  bool //是否进行安全验证
)

func SafetyCheckV2() gin.HandlerFunc {
	//是否进行检测
	return func(c *gin.Context) {
		/*
			if system.SafetyCheckEnable != true {	//配置文件中读取参数，否需要进行安全检测
				c.Next()
				return
			}	*/
		method := c.Request.Method
		if method == http.MethodPost || method == http.MethodPut {
			//strings.HasPrefix( c.Request.RequestURI,"/v2/")
			if !strings.Contains(c.Request.RequestURI, "/v2/") {
				//不含v2，说明是旧接口，不需要判断安全性
				c.Next()
				return
			}
		} else {
			c.Next()
			return
		}

		fmt.Println(c.ContentType())
		//针对重放攻击检查
		attackFlag, err := IsReplayAttact(c)
		if err != nil {
			//mlog.Error(err)
			c.JSON(http.StatusOK, model.Fail("数据包安全性检测未通过"))
			c.Abort()
			return
		}
		//可以补充针对其它攻击检查，...
		//=----------------------
		if attackFlag == true { //识别为攻击
			c.JSON(http.StatusOK, model.Fail("数据包安全性检测未通过"))
			c.Abort()
		} else {
			c.Next()
		}
	}
}

/********
usage: 判断是否重放攻击
return :1、数据包标识 string
        2、是否攻击 bool true 攻击， false 正常数据包
        3、err 异常

20191029	暂时禁用token作因子.
*********/
func IsReplayAttact(c *gin.Context) (bool, error) {
	/*
		rsaTimestamp := c.Request.Header.Get("TIMESTAMP")
		if rsaTimestamp == "" {
			//mlog.Error("there is no timestamp")
			return true, errors.New("there is no timestamp") //无效指令，时间戳超期
		}
		hashNonce := c.Request.Header.Get("NONCE") //hash
		if hashNonce == "" {
			//mlog.Error("there is no NONCE")
			return true, errors.New("there is no NONCE") //无效指令，无指纹
		}
		rsaDeviceNo := c.Request.Header.Get("deviceNo")
		if rsaDeviceNo == "" {
			//mlog.Error("there is no deviceNo") //无效指令，无deviceid
			return true, errors.New("there is no deviceNo")
		}
		rasSequenceNo := c.Request.Header.Get("sequenceno")
		if rasSequenceNo == "" {
			//mlog.Error("there is no sequenceno")
			return true, errors.New("there is no sequenceno")
		}

		timestamp, err := util.RsaDecryptWithDefaultPrivatePem(rsaTimestamp) //解码->时间戳
		if err != nil {
			//mlog.Error(err)
			return true, err //无效指令，解码时间戳失败
		}

		//	if  len(timestamp)!=19 {	//纳秒
		if len(timestamp) != 13 { //毫秒
			return true, errors.New("timestamp error，illegal format") //无效指令，时间戳超期
		}

		timestampInt64, err := util.Str2int64(timestamp) //毫秒
		if err != nil {
			//mlog.Error(err)
			return true, err
		}

		due := util.GetNowMillisecond() - timestampInt64 //毫秒
		//due := time.Now().UnixNano() - timestampInt64
		if due > int64(1000*ApiPackageKeepTime) { //ApiPackageKeepTime为秒
			return true, errors.New("timestamp overdue,invalid request") //无效指令，时间戳超期
		}

		deviceNo, err := util.RsaDecryptWithDefaultPrivatePem(rsaDeviceNo) //解码->device no
		if err != nil {
			//mlog.Error(err, rsaDeviceNo)
			return true, errors.New("deviceNo decode error.")
		}
		sequenceNoStr, err := util.RsaDecryptWithDefaultPrivatePem(rasSequenceNo)
		if err != nil {
			//mlog.Error(err, rasSequenceNo)
			return true, errors.New("sequenceNo decode error")
		}

		sequenceNo, err := util.Str2int64(sequenceNoStr)
		//sequenceNo,err := strconv.ParseInt(sequenceNoStr, 10, 64)
		if err != nil {
			//mlog.Error("seqNo str to int error:", err)
			return true, errors.New("sequenceNo parseint error")
		}

		//nonce = HASH(序号 + 时间戳 + deviceNo),
		//重新计算 nonce2 ，与 nonce 相比较，如果不一致，则可能中间环节被篡改，可认定是非法访问
		nonce2str := sequenceNoStr + timestamp + deviceNo
		sha1Inst := sha1.New()
		_, err = sha1Inst.Write([]byte(nonce2str))
		if err != nil {
			//mlog.Error(err)
			return true, err
		}

		result := sha1Inst.Sum(nil)
		hashNonce2 := fmt.Sprintf("%x", result)
		if hashNonce2 != hashNonce {
			//mlog.Info("IsReplayAttact:nonce=:[", hashNonce, "], timestamp=[", timestamp, "], deviceNo=[", deviceNo, "] seqNo = [", sequenceNo, "] AttackFlag=true")
			return true, errors.New("finger different:")
		}

		apiCountStr, err := redis.String(cache.Do("GET", REDIS_KEY_APIFINGERPRINT_TIMES+hashNonce+c.Request.RequestURI))
		if err != nil {
			//mlog.Info("IsReplayAttact:nonce=:[", hashNonce, "], timestamp=[", timestamp, "], deviceNo=[", deviceNo, "] seqNo = [", sequenceNo, "] AttackFlag=[true],Redis Error=", err)
			RecordAPIPackageFingerprint(hashNonce+c.Request.RequestURI, 1)
			return false, nil //没有找到，说明原来没有,非攻击，可以正常向下执行
		}
		//如果REDIS没有指纹记录，apiCountStr为空
		apiCount, err := util.Str2int(apiCountStr)
		if err != nil || apiCountStr == "" {
			apiCount = 0
		}
		apiCount++

		if apiCount > ApiPackageMaxCnt {
			//mlog.Info("IsReplayAttact,count overflow:nonce=:[", hashNonce, "], timestamp=[", timestamp, "], deviceNo=[", deviceNo, "] seqNo = [", sequenceNo, "] AttackFlag=true")
			RecordAPIPackageFingerprint(hashNonce+c.Request.RequestURI, apiCount)
			return true, errors.New("api count overflow")
		}

		///若干redis中没有，则说明第一次接收
		//mlog.Info("IsReplayAttact:nonce=:[", hashNonce, "], timestamp=[", timestamp, "], deviceNo=[", deviceNo, "] seqNo = [", sequenceNo, "]")
		err = RecordAPIPackageFingerprint(hashNonce+c.Request.RequestURI, apiCount)
		if err != nil {
			//mlog.Info("RecordAPIPackageFingerprint error:", err)
		}
	*/

	return false, nil
}

/*
usage: 记录API数据包指纹和更新计数
input: api指纹 fingerPrint
       次数    count
return :err 异常
*/
func RecordAPIPackageFingerprint(fingerPrint string, count int) error {
	if ApiPackageKeepTime <= 0 || ApiPackageKeepTime > 3600*24 { //保持时间1秒-1天,其它为非法,校正
		ApiPackageKeepTime = 3600 * 2 //缺省2小时吧
	}
	_, err := cache.Do("SETEX", REDIS_KEY_APIFINGERPRINT_TIMES+fingerPrint, ApiPackageKeepTime, count)
	return err
}
